/** PettyXML - A very basic xml parser
  * Copyright (c) 2008 Philipp Gildein (rmbl@openspeak-project.org)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification,
  * are permitted provided that the following conditions are met:
  *
  *    * Redistributions of source code must retain the above copyright notice,
  *         this list of conditions and the following disclaimer.
  *    * Redistributions in binary form must reproduce the above copyright notice,
  *         this list of conditions and the following disclaimer in the documentation
  *         and/or other materials provided with the distribution.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
  * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

#include "pettyxml.hpp"

namespace PettyXML
{

    /** \brief Check if the char is a whitespace character
     *  \param c The char to check
     *  \return True if the char is a whitespace character
     */
    bool isWhiteSpace(char c)
    {
        return (c == ' ' || c == '\r' || c == '\n' || c == '\t');
    }

    /** \brief Strip all trailing whitespace characters
     *  \param text The string to strip
     *  \return The stripped string
     */
    std::string stripTrailingWhiteSpace(const std::string &text)
    {
        if (text.empty())
            return text;

        std::string tmp(text);
        while (isWhiteSpace(tmp[0]))
            tmp = tmp.substr(1);
        return tmp;
    }

    /** \brief Strip the ' and " tags
     *  \param text The string to strip
     *  \return The stripped string
     */
    std::string stripClosingTags(const std::string &text)
    {
        if (text.empty())
            return text;

        std::string tmp(text);
        if (tmp[0] == '\'' || tmp[0] == '"')
            tmp = tmp.substr(1);
        std::string::size_type i = tmp.size()-1;
        if (tmp[i] == '\'' || tmp[i] == '"')
            tmp.erase(i);
        return tmp;
    }

    /** \brief Check if the char is an alphabetic character
     *  \param c The char to check
     *  \return True if the char is an alphabetic character
     */
    bool isAlpha(const char &c)
    {
        if (c < 127)
            return isalpha(c);
        else
            return true;
    }

    /** \brief Check if the char is an alphanumeric character
     *  \param c The char to check
     *  \return True if the char is an alphanumeric character
     */
    bool isAlphaNum(const char &c)
    {
        if (c < 127)
            return isalnum(c);
        else
            return true;
    }

    XMLNode::XMLNode() : mName(), mType(UNKNOWN), mClosed(false)
    {

    }

    XMLNode::XMLNode(const std::string &name) : mName(name), mType(UNKNOWN),
            mClosed(false)
    {

    }

    XMLNode::~XMLNode()
    {
        while (!mChildren.empty())
        {
            delete *mChildren.begin();
            mChildren.erase(mChildren.begin());
        }
    }

    void XMLNode::setAttribute(const std::string &name, const std::string &value)
    {
        mAttributes[name] = value;
    }

    std::string XMLNode::getAttribute(const std::string &name) const
    {
        AttributesMap::const_iterator it = mAttributes.find(name);
        if (it != mAttributes.end())
            return it->second;
        else
            return std::string();
    }

    bool XMLNode::attributeExists(const std::string &name) const
    {
        AttributesMap::const_iterator it = mAttributes.find(name);
        return it == mAttributes.end() ? false : true;
    }
    
    void XMLNode::setText(const std::string &text)
    {
        mText = text;
        if (mType != TEXT)
            mType = TEXT;
    }
    
    void XMLNode::addChild(XMLNode *child)
    {
        if (child && child == this)
            throw XMLException("Can't add node as a child of itself");
        else if (child)
            mChildren.push_back(child);
    }

    void XMLNode::removeChild(XMLNode *child)
    {
        ChildrenVector::iterator it;
        for (it = mChildren.begin(); it != mChildren.end(); ++it)
        {
            if (*it == child)
            {
                delete child;
                mChildren.erase(it);
                return;
            }
        }
    }

    void XMLNode::removeChild(const unsigned int &index)
    {
        if (index < mChildren.size())
        {
            delete mChildren[index];
            mChildren.erase(mChildren.begin() + index);
        }
    }

    XMLNode* XMLNode::findChild(const std::string &child) const
    {
        for (const_iterator it = mChildren.begin(); it != mChildren.end(); ++it)
        {
            if ((*it)->getName() == child)
                return *it;
        }
        return 0;
    }

    bool XMLNode::parse(std::string &rest)
    {
        if (rest.empty())
            return false;

    /* Check if there are other things than xml tags */
        rest = stripTrailingWhiteSpace(rest);
        if (rest[0] != '<')     // Everything other than an xml tag is incorrect
            throw XMLException("Incorrect char in file");

    /* Get the end of the tag */
        std::string::size_type in = rest.find('>');
        if (in == std::string::npos)
            throw XMLException("Never ending tag");

    /* Identify and parse the tag */
        std::string tag = rest.substr(0, in+1);
        mType = parseTag(tag);

    /* If it's a comment remove it and return */
        if (!mType)
        {
            return false;
        }
        else if (mType != ELEMENT)
        {
            rest.erase(0,in+1);
            return true;
        }

        rest.erase(0,in+1);
        if (rest.empty() || mClosed)
            return true;

        while (!rest.empty() && !mClosed)
        {
        /* Search for children and the close tag */
            rest = stripTrailingWhiteSpace(rest);
            if (rest[0] != '<')     // We've got text in here
            {
                std::string text;
                size_t i = 0;
                while (i < rest.size() && rest[i] != '<' && rest[i] != '>')
                    text += rest[i++];

                if (!text.empty())
                {
                    setText(text);
                    mType = TEXT;
                }
                rest.erase(0,i);
                continue;
            }

            if (rest[1] == '/')
            {
                std::string name;
                size_t i = 2;
                while (i < rest.size() && (isAlphaNum(rest[i]) || 
                        rest[i] == '_' || rest[i] == '-' || rest[i] == ':' ||
                        rest[i] == '.'))
                    name += rest[i++];

                if (name.empty())
                {
                    throw XMLException("Empty close tag");
                }
                else if (name == mName)
                {
                    mClosed = true;
                    rest.erase(0,i+1);
                    return true;
                }
            }

        /* Get the end of the tag */
            in = rest.find('>');
            if (in == std::string::npos)
                throw XMLException("Never ending tag");

            XMLNode *node = new XMLNode();
            if (!node->parse(rest))
            {
                delete node;
                return false;
            }
            addChild(node);
        }

        return true;
    }

    void XMLNode::printToFile(std::ostream &file, const unsigned int &col) const
    {
        if (!file.good())
            return;
    /* Start with the < and the name of the tag */
        std::string tabs;
        for (unsigned int i = 0; i < col; ++i)
            tabs += '\t';

        file << tabs << '<' << mName;

        for (AttributesMap::const_iterator it = mAttributes.begin();
                it != mAttributes.end(); ++it)
            file << ' ' << it->first << "=\"" << it->second << '\"';

        if (mChildren.empty() && mText.empty())
        {
            file << " />\n";
            file.flush();
            return;
        }

        file << ">";

        if (!mText.empty())
        {
            file << mText;
            file << (mText[mText.size()-1] != '\n' ? "" : tabs) <<
                    "</" << mName << ">\n";
        }
        else
        {
            file << '\n';
            for (ChildrenVector::const_iterator it = mChildren.begin();
                    it != mChildren.end(); ++it)
                (*it)->printToFile(file, col+1);
            file << tabs << "</" << mName << ">\n";
        }

        file.flush();
    }

    XMLNode::Type XMLNode::identify(const std::string &tag) const
    {
        if (tag.empty() || tag.size() < 2)
            return UNKNOWN;

    /* Check the beginning */
        if (tag[0] != '<')
        {
            throw XMLException("Invalid char in file");
        }
        else if (tag.find("<!--") == 0)
        {
            if (tag.rfind("-->") == tag.size()-3)
                return COMMENT;
            else
                throw XMLException("Incorrect Comment");
        }
        else if (tag.find("<?xml") == 0)
        {
            return DEFINITION;
        }
        else if (tag.find("<!") == 0)
        {
            return DTD;
        }
        else if (isalpha(tag[1]) || tag[1] == '_' || tag[1] == '/')
        {
            return ELEMENT;
        }

        return UNKNOWN;
    }

    XMLNode::Type XMLNode::parseTag(const std::string &tag)
    {
        if (tag.empty())
            return UNKNOWN;

    /* Identify the tag */
        Type type = identify(tag);
        if (!type)
            return UNKNOWN;
        else if (type != ELEMENT)
            return type;

    /* Get the name */
        size_t i = 1;
        std::string name;
        if (!isAlpha(tag[i]) && tag[i] != '_')  // XML tags may only begin with an alpha character or '_'
            throw XMLException("Incorrect char at the beginning of the tag");

        while (i < tag.size() && (isAlphaNum(tag[i]) || tag[i] == '_' ||
                tag[i] == '-' || tag[i] == ':' || tag[i] == '.'))
            name += tag[i++];

        if (!name.empty())
            setName(name);
        std::string rest = stripTrailingWhiteSpace(tag.substr(i));

        if (rest.empty())
        {
            return UNKNOWN;
        }
        else if (rest[0] == '/')
        {
            if (rest.size() == 2 && rest[1] == '>')
            {
                mClosed = true;
                return type;
            }
            else
            {
                throw XMLException("Incorrect usage of /");
            }
        }

    /* There must be some attributes, find them */
        while (!rest.empty())
        {
        /* Check first if the tag ends here */
            if (rest[0] == '>')
            {
                return type;
            }
            else if (rest[0] == '/')
            {
                if (rest.size() == 2 && rest[1] == '>')
                {
                    mClosed = true;
                    return type;
                }
                else
                {
                    throw XMLException("Incorrect usage of /");
                }
            }

            std::string value;
            name.clear();
            i = 0;
        /* Get the name of the attribute. Tags may only begin with an alphabetic
           character or '_'. */
            if (!isAlpha(rest[i]) && rest[i] != '_')
                throw XMLException("Incorrect char at the beginning of the tag");

            while (i < rest.size() && (isAlphaNum(rest[i]) ||
                    rest[i] == '_' || rest[i] == '-' || rest[i] == ':' ||
                    rest[i] == '.' ))
                name += rest[i++];

            if (name.empty() || rest[i] != '=')
                throw XMLException("Incorrect Attribute in tag");

        /* Get the value */
            ++i;
            char delim = 0;
            if (rest[i] == '\"' || rest[i] == '\'')
                delim = rest[i];
            else
                throw XMLException("Missing delimiter in attribute");

            value = rest[i++];
            while (i < rest.size() && (rest[i] != delim))
                value += rest[i++];

            if (i < rest.size() && rest[i] == delim)
                value += rest[i++];
            else if (i == rest.size())
                throw XMLException("Missing delimiter at the end of the attribute");

            setAttribute(name, stripClosingTags(value));
            rest = stripTrailingWhiteSpace(rest.substr(i));
        }

        return type;
    }

    XMLDocument::XMLDocument(const std::string &file) : mFile(file)
    {

    }

    XMLDocument::XMLDocument() : mFile()
    {

    }

    XMLDocument::~XMLDocument()
    {

    }

    bool XMLDocument::parse()
    {
        if (mFile.empty())
            return false;
        std::ifstream filesock;

        filesock.open(mFile.c_str(), std::ios::in);
        if (!filesock.is_open())
            return false;

        std::string file;
        while (!filesock.eof())
        {
            std::string line;
            getline(filesock, line);
            file += line + "\n";
        }
        file.erase(file.size()-1);
        filesock.close();

        return !file.empty() ? parse(file) : false;
    }

    bool XMLDocument::parse(const std::string &xml)
    {
        if (xml.empty())
            return false;

        std::string file = stripTrailingWhiteSpace(xml);
        while (file[0] == '<')
        {
            XMLNode *node = new XMLNode();
            if (!node->parse(file))
                return false;
            addChild(node);
            file = stripTrailingWhiteSpace(file);
        }
        if (!stripTrailingWhiteSpace(file).empty())
            throw XMLException("Invalid char at the end of file "+mFile);

        return true;
    }

    bool XMLDocument::save()
    {
        std::ofstream filesock;

        filesock.open(mFile.c_str(), std::ios::out);
        if (!filesock.is_open())
            return false;

        bool ret = save(filesock);
        filesock.close();
        return ret;
    }

    bool XMLDocument::save(std::ostream &out)
    {
        if (!out.good())
            return false;

        for (const_iterator it = begin(); it != end(); ++it)
            (*it)->printToFile(out, 0);

        out.flush();
        return true;
    }

}
